using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Windows.Forms;
using CSW.Framework.Common;
using CSW.Framework.Common.Binding;
using CSW.Framework.Common.Binding.Events;
using CSW.Framework.Common.Binding.Interfaces;
using CSW.Framework.Common.Binding.ValueTypes;

namespace CSW.Framework.UI.Binding
{
    /// <summary>
    /// Provides capability to bind controls and error providers to properties in an <see cref="IBindableObject" />.
    /// </summary>
    public class Binder : IDisposable
    {
        private readonly IBindableObject m_BindableObject;
        private readonly Dictionary<string, MethodInvoker> m_PropertyRetrievers;
        private readonly Dictionary<string, IBindableControl> m_PropertyControls;
        private readonly IErrorWrapper m_ErrorProvider;
        private readonly Dictionary<string, Func<bool>> m_PropertyReadOnly;
        private readonly Dictionary<string, string> m_CapturedErrors = new Dictionary<string, string>();
        private readonly List<IBusinessObject> m_RefreshList = new List<IBusinessObject>();
        private bool m_ShowErrors;
        private bool m_IsReadOnly;

        private delegate bool TryParseDelegate<T>(string str, out T value);

        /// <summary>
        /// TryPersistDelegate. See <see cref="TryPersist"/>.
        /// </summary>
        protected delegate bool TryPersistDelegate(out string errorMessage);

        /// <summary>
        /// Initializes a new instance of the <see cref="Binder"/> class.
        /// </summary>
        /// <param name="bindableObject">The bindable object to bind to.</param>
        /// <param name="errorProvider">The error provider.</param>
        public Binder(IBindableObject bindableObject, IErrorWrapper errorProvider)
            : this(bindableObject, errorProvider, Binding.ShowErrors.Immediately)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Binder"/> class.
        /// </summary>
        /// <param name="bindableObject">The bindable object to bind to.</param>
        /// <param name="errorProvider">The error provider.</param>
        /// <param name="showErrors">if set to 
        /// <see cref="CSW.Framework.UI.Binding.ShowErrors.AfterSave"/>,
        /// show errors only after save attempted
        /// (default = <see cref="CSW.Framework.UI.Binding.ShowErrors.Immediately"/>).
        /// </param>
        public Binder(IBindableObject bindableObject, IErrorWrapper errorProvider, ShowErrors showErrors)
        {
            Guard.ArgumentNotNull(bindableObject, "bindableObject");
            Guard.ArgumentNotNull(errorProvider, "errorProvider");

            m_BindableObject = bindableObject;
            m_ErrorProvider = errorProvider;
            m_ShowErrors = (showErrors == Binding.ShowErrors.Immediately);

            m_BindableObject.PropertyChanged += BindableObject_PropertyChanged;
            m_PropertyRetrievers = new Dictionary<string, MethodInvoker>();
            m_PropertyControls = new Dictionary<string, IBindableControl>();
            m_PropertyReadOnly = new Dictionary<string, Func<bool>>();

            bindableObject.InvalidData += NotifyInvalidData_InvalidData;
            bindableObject.ReadOnlyChanged += BindableObject_ReadOnlyChanged;
        }

        /// <summary>
        /// Gets or sets a value indicating whether or not validation errors will currently be shown on controls.
        /// </summary>
        /// <value><c>true</c> if validation errors will be shown on controls; otherwise, <c>false</c>.</value>
        public bool ShowErrors
        {
            get { return m_ShowErrors; }
            protected set { m_ShowErrors = value; }
        }

        /// <summary>
        /// Gets the bindable object.
        /// </summary>
        /// <value>The bindable object.</value>
        public IBindableObject BindableObject
        {
            get { return m_BindableObject; }
        }

        /// <summary>
        /// Validates all controls.
        /// </summary>
        public void ValidateAllControls()
        {
            foreach (IBindableControl control in m_PropertyControls.Values)
            {
                control.SendValidated();
            }
        }

        /// <summary>
        /// Makes all controls read only.
        /// </summary>
        public void MakeReadOnlyAndUpdateControls()
        {
            m_IsReadOnly = true;
            UpdateControls();
        }

        private void BindableObject_ReadOnlyChanged(object sender, ReadOnlyChangedEventArgs e)
        {
            IBindableControl bindableControl;
            if (m_PropertyControls.TryGetValue(e.PropertyName, out bindableControl))
            {
                bindableControl.ReadOnly = e.IsReadOnly;
            }
        }

        private void NotifyInvalidData_InvalidData(object sender, InvalidDataEventArgs e)
        {
            if (m_ShowErrors)
            {
                IBindableControl control;
                if (m_PropertyControls.TryGetValue(e.Property, out control))
                {
                    m_ErrorProvider.SetError(control.Control, e.Message, e.ErrorType);
                }
            }
        }

        private void BindableObject_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            MethodInvoker retrieveProperty;
            if (m_PropertyRetrievers.TryGetValue(e.PropertyName, out retrieveProperty))
            {
                retrieveProperty();
            }
        }

        private void TryParse<T>(TryParseDelegate<T> tryParse, IBindableControl bindableControl, ComplexProperty complexProperty, bool allowNull)
        {
            Guard.ArgumentNotNull(tryParse, "tryParse");
            Guard.ArgumentNotNull(bindableControl, "bindableControl");
            Guard.ArgumentNotNull(complexProperty, "complexProperty");

            if (m_IsReadOnly)
                return;

            try
            {
                T result;
                if (tryParse((string)bindableControl.Value, out result))
                {
                    complexProperty.UnsafeValue = result;
                }
                else
                {
                    if (allowNull)
                        complexProperty.UnsafeValue = null;
                    else
                        bindableControl.Value = complexProperty.UnsafeValue;
                }
            }
            catch (TargetInvocationException ex)
            {
                bindableControl.Value = complexProperty.UnsafeValue;
                throw ex.InnerException;
            }
            catch
            {
                bindableControl.Value = complexProperty.UnsafeValue;
                throw;
            }
        }

        /// <summary>
        /// Binds a control to a property in the bindable object.
        /// </summary>
        /// <param name="bindableControl">The bindable control.</param>
        /// <param name="propertyName">Name of the property.</param>
        public virtual void Bind(IBindableControl bindableControl, string propertyName)
        {
            Guard.ArgumentNotNull(bindableControl, "bindableControl");
            Guard.ArgumentNotNullOrEmptyString(propertyName, "propertyName");

            ComplexProperty complexProperty = GetComplexProperty(m_BindableObject, propertyName);
            bool noProperty = ((object)complexProperty == null);
            
            // Add the control and property name to the dictionary
            m_PropertyControls.Add(propertyName, bindableControl);
            m_PropertyReadOnly.Add(propertyName, () => (object)complexProperty == null ? true : complexProperty.IsReadOnly);

            if (noProperty)
                return;

            // MaxLength
            if (complexProperty.MaxLength.HasValue)
            {
                bindableControl.MaxLength = complexProperty.MaxLength.Value;
            }

            // Get
            m_PropertyRetrievers.Add(propertyName, () => bindableControl.Value = complexProperty.UnsafeValue);

            // signed
            Type valueType = complexProperty.ValueType;
            if (valueType == typeof(int))
            {
                bindableControl.Validated += delegate { TryParse<int>(int.TryParse, bindableControl, complexProperty, false); };
            }
            else if (valueType == typeof(int?))
            {
                bindableControl.Validated += delegate { TryParse<int>(int.TryParse, bindableControl, complexProperty, true); };
            }
            else if (valueType == typeof(long))
            {
                bindableControl.Validated += delegate { TryParse<long>(long.TryParse, bindableControl, complexProperty, false); };
            }
            else if (valueType == typeof(long?))
            {
                bindableControl.Validated += delegate { TryParse<long>(long.TryParse, bindableControl, complexProperty, true); };
            }
            else if (valueType == typeof(short))
            {
                bindableControl.Validated += delegate { TryParse<short>(short.TryParse, bindableControl, complexProperty, false); };
            }
            else if (valueType == typeof(short?))
            {
                bindableControl.Validated += delegate { TryParse<short>(short.TryParse, bindableControl, complexProperty, true); };
            }
            // unsigned
            else if (valueType == typeof(uint))
            {
                bindableControl.Validated += delegate { TryParse<uint>(uint.TryParse, bindableControl, complexProperty, false); };
            }
            else if (valueType == typeof(uint?))
            {
                bindableControl.Validated += delegate { TryParse<uint>(uint.TryParse, bindableControl, complexProperty, true); };
            }
            else if (valueType == typeof(ulong))
            {
                bindableControl.Validated += delegate { TryParse<ulong>(ulong.TryParse, bindableControl, complexProperty, false); };
            }
            else if (valueType == typeof(ulong?))
            {
                bindableControl.Validated += delegate { TryParse<ulong>(ulong.TryParse, bindableControl, complexProperty, true); };
            }
            else if (valueType == typeof(ushort))
            {
                bindableControl.Validated += delegate { TryParse<ushort>(ushort.TryParse, bindableControl, complexProperty, false); };
            }
            else if (valueType == typeof(ushort?))
            {
                bindableControl.Validated += delegate { TryParse<ushort>(ushort.TryParse, bindableControl, complexProperty, true); };
            }
            // double/float/decimal
            else if (valueType == typeof(double))
            {
                bindableControl.Validated += delegate { TryParse<double>(double.TryParse, bindableControl, complexProperty, false); };
            }
            else if (valueType == typeof(double?))
            {
                bindableControl.Validated += delegate { TryParse<double>(double.TryParse, bindableControl, complexProperty, true); };
            }
            else if (valueType == typeof(float))
            {
                bindableControl.Validated += delegate { TryParse<float>(float.TryParse, bindableControl, complexProperty, false); };
            }
            else if (valueType == typeof(float?))
            {
                bindableControl.Validated += delegate { TryParse<float>(float.TryParse, bindableControl, complexProperty, true); };
            }
            else if (valueType == typeof(decimal))
            {
                bindableControl.Validated += delegate { TryParse<decimal>(decimal.TryParse, bindableControl, complexProperty, false); };
            }
            else if (valueType == typeof(decimal?))
            {
                bindableControl.Validated += delegate { TryParse<decimal>(decimal.TryParse, bindableControl, complexProperty, true); };
            }
            else
            {
                // string, bool, bool?, DateTime, DateTime?, and all other types
                // including custom types.

                // Set
                bindableControl.Validated += delegate
                                                 {
                                                     if (m_IsReadOnly)
                                                         return;

                                                     try
                                                     {
                                                         complexProperty.UnsafeValue = bindableControl.Value;
                                                     }
                                                     catch (TargetInvocationException ex)
                                                     {
                                                         bindableControl.Value = complexProperty.UnsafeValue;
                                                         throw ex.InnerException;
                                                     }
                                                     catch
                                                     {
                                                         bindableControl.Value = complexProperty.UnsafeValue;
                                                         throw;
                                                     }
                                                 };
            }
        }

        private ComplexProperty GetComplexProperty(IBindableObject bindableObject, string propertyName)
        {
            Guard.ArgumentNotNull(bindableObject, "bindableObject");
            Guard.ArgumentNotNullOrEmptyString(propertyName, "propertyName");

            if (propertyName.Contains("."))
            {
                m_IsReadOnly = true;

                string[] props = propertyName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
                string newPropertyName = string.Empty;
                for (int i = 1; i < props.Length; i++)
                {
                    if (i != 1)
                        newPropertyName += ".";
                    newPropertyName += props[i];
                }

                string firstPropName = props[0];
                ComplexProperty firstComplexProp = bindableObject.Properties[firstPropName];
                IBindableObject newBindableObject = firstComplexProp.UnsafeValue as IBindableObject;
                if (newBindableObject == null)
                    return null;

                IBusinessObject newBusinessObject = newBindableObject as IBusinessObject;
                if (newBusinessObject != null)
                {
                    if (!m_RefreshList.Contains(newBusinessObject))
                    {
                        newBusinessObject.Refresh();
                        m_RefreshList.Add(newBusinessObject);
                    }
                }

                return GetComplexProperty(newBindableObject, newPropertyName);
            }
            else
            {
                try
                {
                    return bindableObject.Properties[propertyName];
                }
                catch (Exception ex)
                {
                    Trace.WriteLine(ex);
                    throw;
                }
                
            }
        }

        /// <summary>
        /// Focuses the control belonging to the specified property.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        public void FocusControl(string propertyName)
        {
            IBindableControl control;
            if (m_PropertyControls.TryGetValue(propertyName, out control))
            {
                control.Focus();
            }
        }

        /// <summary>
        /// Updates all controls.
        /// </summary>
        public virtual void UpdateControls()
        {
            foreach (KeyValuePair<string, MethodInvoker> kvp in m_PropertyRetrievers)
            {
                try
                {
                    kvp.Value();
                }
                catch (Exception ex)
                {
                    throw new Exception(string.Format("Property retriever for '{0}' threw an exception.", kvp.Key), ex);
                }
            }

            foreach (KeyValuePair<string, Func<bool>> kvp in m_PropertyReadOnly)
            {
                IBindableControl bindableControl;
                if (m_PropertyControls.TryGetValue(kvp.Key, out bindableControl))
                    bindableControl.ReadOnly = m_IsReadOnly ? true : kvp.Value();
            }
        }

        private void CaptureErrors(object sender, InvalidDataEventArgs e)
        {
            if (e.ErrorType != ErrorType.None)
            {
                // This method captures all errors raised by calling TrySave()
                m_CapturedErrors.Add(e.Property, e.Message);
            }
        }

        private void ClearCapturedErrors()
        {
            m_CapturedErrors.Clear();
        }

        /// <summary>
        /// Tries to save the current state of the bound item as the baseline state, without saving to the database.
        /// </summary>
        /// <param name="verbose">if set to <c>true</c> a message box will be displayed if there are business rule which fail.</param>
        /// <returns><c>true</c> if successful; otherwise, <c>false</c>.</returns>
        public bool TrySaveBaselineState(bool verbose)
        {
            m_ShowErrors = true;

            string errorMessage;
            bool success = TrySaveBaselineState(out errorMessage);
            if (verbose && !success)
                MessageBox.Show(errorMessage, "", MessageBoxButtons.OK, MessageBoxIcon.Information);
            return success;
        }

        /// <summary>
        /// Tries to save the current state of the bound item as the baseline state, without saving to the database.
        /// </summary>
        /// <param name="errorMessage">The error message.</param>
        /// <returns><c>true</c> if successful; otherwise, <c>false</c>.</returns>
        public bool TrySaveBaselineState(out string errorMessage)
        {
            return TryPersist(out errorMessage, m_BindableObject.TrySaveBaselineState);
        }

        /// <summary>
        /// Tries to persist the current state using the <paramref name="tryPersistDelegate"/> parameter.
        /// </summary>
        /// <param name="errorMessage">The error message.</param>
        /// <param name="tryPersistDelegate">The <see cref="TryPersistDelegate"/>.</param>
        /// <returns><c>true</c> if successful; otherwise, <c>false</c>.</returns>
        protected bool TryPersist(out string errorMessage, TryPersistDelegate tryPersistDelegate)
        {
            Guard.ArgumentNotNull(tryPersistDelegate, "tryPersistDelegate");

            Cursor.Current = Cursors.WaitCursor;

            m_ShowErrors = true;

            ClearCapturedErrors();
            bool success;
            m_BindableObject.InvalidData += CaptureErrors;

            try
            {
                string returnedErrorMessage;
                success = tryPersistDelegate(out returnedErrorMessage);

                // TODO: May need work.
                if (!success)
                {
                    errorMessage = string.Empty;
                    IBindableControl firstControl = null;
                    foreach (KeyValuePair<string, string> kvp in m_CapturedErrors)
                    {
                        if (!string.IsNullOrEmpty(errorMessage)) errorMessage += "\n";
                        errorMessage += kvp.Value;

                        IBindableControl control;
                        if (m_PropertyControls.TryGetValue(kvp.Key, out control))
                        {
                            if (firstControl == null) firstControl = control;
                            else if (GetTabIndex(control) < GetTabIndex(firstControl)) firstControl = control;
                        }
                    }

                    if (firstControl != null)
                        firstControl.Focus();

                    if (string.IsNullOrEmpty(errorMessage))
                        errorMessage = returnedErrorMessage;
                }
                else
                {
                    errorMessage = null;
                }
            }
            finally
            {
                m_BindableObject.InvalidData -= CaptureErrors;
                Cursor.Current = Cursors.Default;
            }

            return success;
        }

        /// <summary>
        /// Tries to validate the bound item.
        /// </summary>
        /// <param name="verbose">if set to <c>true</c> a message box will be displayed if there are business rule which fail.</param>
        /// <returns><c>true</c> if successful; otherwise, <c>false</c>.</returns>
        public bool Validate(bool verbose)
        {
            m_ShowErrors = true;

            string errorMessage;
            bool success = Validate(out errorMessage);
            if (verbose && !success)
                MessageBox.Show(errorMessage, "", MessageBoxButtons.OK, MessageBoxIcon.Information);
            return success;
        }

        /// <summary>
        /// Tries to validate the bound item.
        /// </summary>
        /// <param name="errorMessage">The error message.</param>
        /// <returns><c>true</c> if successful; otherwise, <c>false</c>.</returns>
        public bool Validate(out string errorMessage)
        {
            return TryPersist(out errorMessage, m_BindableObject.CheckAllRules);
        }

        private static int GetTabIndex(IBindableControl bindableControl)
        {
            Guard.ArgumentNotNull(bindableControl, "bindableControl");

            // TODO: This isn't exact due to nested controls
            int tabIndex = 0;
            Control control = bindableControl.Control;
            int mul = 1;
            while (control != null)
            {
                tabIndex += (control.TabIndex * mul) + control.TabIndex;
                control = control.Parent;
                mul *= 1000;
            }
            return tabIndex;
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public virtual void Dispose()
        {
            m_BindableObject.PropertyChanged -= BindableObject_PropertyChanged;
            m_BindableObject.InvalidData -= NotifyInvalidData_InvalidData;
            m_BindableObject.ReadOnlyChanged -= BindableObject_ReadOnlyChanged;

            foreach (IBindableControl bindableControl in m_PropertyControls.Values)
            {
                bindableControl.Dispose();
            }
        }
    }
}